6.11. Принципы компонентной архитектуры
Принципы компонентной архитектуры
Когда проект выходит за пределы одного модуля или репозитория, возникает необходимость в компонентах — независимо выпускаемых, повторно используемых единицах кода: NuGet-пакеты, npm-модули, JAR-библиотеки, Docker-образы с чётким API.
Компонент — это автономная программная единица с чётко определённым назначением, интерфейсом и поведением, способная функционировать независимо или в составе более крупной системы.
Репозиторий — централизованное хранилище исходного кода, метаданных и истории изменений, предназначенное для совместной разработки, контроля версий и управления жизненным циклом программного обеспечения.
Единица кода — минимальный логически завершённый фрагмент программного текста, реализующий конкретную функциональность и подлежащий отдельному тестированию, сборке или повторному использованию.
NuGet-пакет — дистрибутив программной библиотеки или инструмента для экосистемы .NET, содержащий скомпилированные сборки, метаданные и зависимости, распространяемый через NuGet-репозиторий.
npm-модуль — пакет программного кода, написанного на JavaScript или TypeScript, предназначенный для использования в Node.js или браузерных средах и распространяемый через реестр npm.
JAR-библиотека — архивный файл формата JAR, содержащий скомпилированный байт-код Java, ресурсы и метаданные, предназначенный для повторного использования в приложениях на платформе Java.
Docker-образ — неизменяемый шаблон файловой системы и конфигурации, описывающий среду выполнения приложения, включая зависимости, библиотеки и параметры запуска, используемый для создания контейнеров.
Роберт Мартин в «Чистой архитектуре» выделил три принципа, определяющих, как следует формировать такие компоненты. Они — аналог SOLID, но применительно к уровням выше классов: к пакетам, модулям, библиотекам.
1. REP — Reuse / Release Equivalence Principle
(Принцип эквивалентности повторного использования и выпуска)
Эквивалентность — свойство двух компонентов или реализаций, при котором они обеспечивают одинаковое поведение, интерфейс и результаты при одинаковых входных условиях.
Повторное использование — практика применения ранее разработанной программной единицы в новых контекстах без её модификации, с сохранением исходной функциональности и надёжности.
Выпуск — официальная версия программного компонента, прошедшая проверку качества, документирована и доступна для развёртывания или интеграции в другие системы.
Формулировка: Классы и модули, предназначенные для повторного использования, должны группироваться в компоненты, которые выпускаются и управляются как единое целое.
Другими словами: если вы хотите использовать некоторую функциональность в нескольких проектах, её нельзя просто копировать файлами. Она должна быть оформлена как отдельный компонент с версионированием, документированным API и процессом релиза.
Почему это важно:
-
Согласованность версий. Если модуль
PaymentCoreиспользуется в трёх сервисах, и в нём исправлен баг, все три сервиса должны обновиться до одной и той же версии. Без пакетного управления легко возникнет ситуация: сервис A использует v1.2, сервис B — v1.3, сервис C — форкнутую копию. Это ведёт к расхождению поведения и трудноуловимым ошибкам. -
Ясность контракта. У компонента есть публичный API и внутренняя реализация. Изменение публичного API требует смены мажорной версии (согласно SemVer). Это заставляет задумываться: действительно ли нужно менять интерфейс? или можно расширить его совместимым образом?
-
Ответственность за качество. Выпуск компонента — это обязательство. Перед релизом должны быть: тесты, документация, проверка обратной совместимости. Это формирует культуру.
Согласованность — соответствие поведения, структуры и интерфейсов компонентов установленным соглашениям, стандартам и ожиданиям пользователей или других систем.
Ясность — свойство компонента, при котором его назначение, интерфейс, зависимости и логика работы легко понимаются без дополнительных пояснений или анализа внутренней реализации.
Контракт — формальное или неформальное описание взаимодействия между компонентами, включающее входные и выходные данные, допустимые состояния, ошибки и правила вызова.
Качество — совокупность характеристик компонента, определяющих его пригодность к эксплуатации, включая корректность, надёжность, производительность, безопасность и удобство сопровождения.
REP говорит: если вы планируете переиспользовать — делайте это правильно. В противном случае — оставайтесь в рамках одного приложения, где управление зависимостями проще.
2. CCP — Common Closure Principle
(Принцип общей закрытости)
Закрытость — принцип проектирования, согласно которому компонент предоставляет только необходимые интерфейсы для взаимодействия, скрывая внутреннюю реализацию от внешнего влияния.
Формулировка: Классы, которые меняются по одним и тем же причинам и в одно и то же время, должны находиться в одном компоненте.
Это — масштабирование SRP (Single Responsibility Principle) на уровень компонентов. SRP говорит: у класса одна причина для изменения. CCP говорит: у компонента — один мотив для перекомпиляции и релиза.
Единственная ответственность — принцип, согласно которому компонент решает одну конкретную задачу и несёт ответственность за одно логическое назначение.
Пример. Рассмотрим систему с модулями:
OrderValidation— проверка корректности заказа;TaxCalculation— расчёт налогов;InvoiceGeneration— формирование счёта;PdfRenderer— генерация PDF.
Можно сгруппировать так:
- Компонент
Billing:TaxCalculation,InvoiceGeneration,PdfRenderer - Компонент
Orders:OrderValidation
Почему? Потому что налоговые правила, формат счёта и требования к PDF часто меняются одновременно — из-за изменений в законодательстве. Размещение их в одном компоненте означает, что обновление налоговой ставки требует одного релиза, а не трёх. При этом валидация заказа зависит от бизнес-требований к корзине и может меняться независимо — например, при введении новых типов доставки.
CCP помогает избежать двух крайностей:
- Чрезмерной дробности — по одному классу на компонент. Тогда любое изменение требует обновления десятков пакетов, что неэффективно.
- Монолитных компонентов — «всё в одном». Тогда даже мелкое изменение в логике валидации требует пересборки и релиза модуля, отвечающего за генерацию отчётов.
Дробность — степень разделения системы на мелкие, слабосвязанные и независимо управляемые компоненты, каждый из которых выполняет узкую функцию.
Монолитность — архитектурный подход, при котором вся функциональность системы реализована в рамках единого исполняемого модуля без явного разделения на независимые компоненты.
Ключевой вопрос при применении CCP: кто инициирует изменение? Если изменения инициируются одной командой (например, финансовой), то логика, которой владеет эта команда, должна быть в одном компоненте — даже если технически она разнородна.
3. CRP — Common Reuse Principle
(Принцип общей повторяемости)
Формулировка: Классы, не предназначенные для совместного использования, не должны находиться в одном компоненте.
Совместное использование — возможность одновременного применения одного и того же компонента несколькими системами, процессами или пользователями без конфликтов и деградации качества.
Это — аналог ISP (Interface Segregation Principle), но на уровне компонентов. Если часть компонента используется, а другая — нет, то пользователь вынужден тащить «мёртвый груз», создавая избыточные зависимости.
Пример. Допустим, есть компонент Utils, содержащий:
StringHelper— утилиты для работы со строками;EncryptionService— шифрование данных;LoggingDecorator— декоратор для логирования.
Сервису, которому нужна только StringHelper, всё равно придётся ссылаться на Utils. Если в EncryptionService появится зависимость от внешней библиотеки (например, Bouncy Castle), то все потребители Utils унаследуют эту зависимость — даже если они никогда не шифруют.
CRP требует: если часть функционала используется независимо — выносите её в отдельный компонент.
StringUtilsCryptoCoreTelemetry
Это снижает связанность: сервисы теперь зависят только от того, что им действительно нужно. Но есть цена: больше компонентов — больше накладных расходов на управление. Поэтому CRP применяется селективно: там, где зависимость критична (например, безопасность, лицензирование), или где повторное использование частично, но массово.
Баланс между CCP и CRP
REP, CCP и CRP не всегда совместимы. CCP толкает к объединению, CRP — к разъединению. Выбор — это компромисс, формализуемый через коэффициент устойчивости (Stability Metric), где есть число входящих зависимостей (сколько компонентов зависит от данного) и число исходящих зависимостей (от скольких компонентов зависит данный). Компонент нестабилен — ни от кого не зависит, но и никто не зависит от него (листья дерева), а компонент устойчив — на него много зависимостей, но он почти ни от кого не зависит (ядра, фреймворки).
Устойчивость — способность компонента сохранять работоспособность, корректность и предсказуемое поведение при изменениях окружения, нагрузки или частичных сбоях зависимостей.
Устойчивые компоненты (например, DomainModel, CommonTypes) должны быть спроектированы так, чтобы их интерфейсы менялись редко — иначе обновление повлечёт каскад изменений. Нестабильные (например, Adapters.Http, UI.Web) могут меняться часто — они «поглощают» нестабильность окружения.
Оптимальная структура — это направленный ациклический граф зависимостей, где устойчивые компоненты находятся внизу, нестабильные — наверху, и зависимости направлены сверху вниз. Циклов быть не должно — они означают, что границы проведены некорректно.
Оптимальность — свойство компонента, при котором он достигает наилучшего баланса между потреблением ресурсов, скоростью выполнения, читаемостью и сопровождаемостью.
Связь с DDD
Принципы компонентной архитектуры естественно ложатся на концепцию ограниченных контекстов (Bounded Contexts) из Domain-Driven Design:
- Один ограниченный контекст — один компонент (или группа тесно связанных компонентов по CCP);
- Публичный API контекста — порт (по гексагональной архитектуре);
- Антикоррупционный слой (ACL) — адаптеры, реализующие интеграцию с другими контекстами;
- Совместное использование типов между контекстами — нарушение CRP, если эти типы не являются действительно общими (например,
Money,DateTimeRange).
Ограниченный контекст — область применения компонента, в пределах которой он имеет чёткое назначение, правила использования и границы ответственности, не выходящие за рамки конкретной предметной области.
В такой модели компонент — это граница семантической целостности. Внутри контекста термины имеют однозначный смысл; на стыке — требуется явное преобразование.